package com.xfcy.blog.config.intercept;

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import com.xfcy.blog.common.constant.Constant;
import com.xfcy.blog.mapper.UserMapper;
import com.xfcy.blog.utils.IpUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 通过guava布隆过滤器实现黑白名单
 *
 * @author 晓风残月Lx
 * @date 2023/6/5 23:13
 */
@Component
public class AccessLimitIntercept implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(AccessLimitIntercept.class);

    @Resource
    private UserMapper userMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    @Value("${IpLimit.count}")
    private int count;
    @Value("${IpLimit.time}")
    private int time;
    @Value("${IpLimit.blackCount}")
    private int blackCount;

    // 预估数据
    public static long SIZE = 1000L;

    // 误判率，它越小误判的个数也越少（思考：是否可以无限小？ 没有误判岂不是更好）
    public static double fpp = 0.03;  // 这个数越小所用的hash函数越多，bitmap占用的位越多  默认的就是0.03，5个hash函数   0.01，7个hash函数

    // 创建白名单guava布隆过滤器
    private static BloomFilter<String> whiteBloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), SIZE, fpp);

    // TODO 创建黑名单guava布隆过滤器
    private static BloomFilter<String> blackBloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), SIZE, fpp);

    /**
     * 接口调用前检查对方ip是否频繁调用接口,如果高频繁调用，直接放入黑名单
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String ip = IpUtils.getIp(request);
        // 如果IP是白名单，直接返回true
        if (whiteBloomFilter.mightContain(ip)) {
            return true;
        } else if (blackBloomFilter.mightContain(ip)) {
            // 如果ip在黑名单，直接返回false
            return false;
        }

        try {
            // handler 是否为 HandleMethod 实例
            if (handler instanceof HandlerMethod) {
                // 除去可能会高度调用的请求
//            String requestURI = request.getRequestURI();
//            if (requestURI.contains("searchArticle")) {
//                return true;
//            }
                // 拼接redis key = IP + Api限流
                String key = ip + request.getRequestURI();
                // 获取redis的value
                Integer maxTimes = null;
                Object value = redisTemplate.opsForValue().get(key);
                if (value != null) {
                    maxTimes = (Integer) value;
                }
                if (maxTimes == null) {
                    // 如果redis中没有该ip对应的时间则表示第一次调用，保存key到redis
                    redisTemplate.opsForValue().set(key, Constant.ONE, time, TimeUnit.SECONDS);
                } else if (maxTimes < count) {
                    // 如果redis中的时间比注解上的时间小则表示可以允许访问,这是修改redis的value时间
                    redisTemplate.opsForValue().set(key, maxTimes + Constant.ONE, time, TimeUnit.SECONDS);
                } else if (maxTimes > blackCount) {
                    logger.info("API请求受到了高频访问,{}，将该IP加入黑名单", key);
                    output(response, "{\"code\":\"8001\",\"message\":\"您已被加入了黑名单\"}");
                    blackBloomFilter.put(ip);
                } else {
                    // 请求过于频繁
                    redisTemplate.opsForValue().set(key, maxTimes + Constant.ONE, time, TimeUnit.SECONDS);
                    logger.info("API请求限流拦截启动,{} 请求过于频繁", key);
                    output(response, "{\"code\":\"8002\",\"message\":\"请求过于频繁，请稍后再试\"}");
                }
            }
        } catch (Exception e) {
            logger.error("API请求限流拦截异常，异常原因：", e);
            e.printStackTrace();
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }

    // 启动时构造白名单
    @PostConstruct
    public void bloomer() {
        List<String> ips = userMapper.selectAllIpAddress();
        for (String ip : ips) {
            whiteBloomFilter.put(ip);
        }
    }

    public void output(HttpServletResponse response, String msg) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.write(msg.getBytes(StandardCharsets.UTF_8));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (ObjectUtils.isNotEmpty(outputStream)) {
                outputStream.flush();
                outputStream.close();
            }
        }
    }

}
